/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.version.model;

import java.util.HashSet;
import java.util.Random;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import offset.nodes.Constants;
import offset.nodes.server.model.RepositoryModel;
import offset.nodes.server.model.RepositoryStartupExtension;

/**
 *
 * @author Walter Luetgenau
 */
public class CheckinStartupExtension implements RepositoryStartupExtension {

    Session systemSession;

    /**
     * Register an event listener for creation and modification stamps.
     * Called by the RepositoryExtensionsManager on startup.
     * 
     * @param systemSession
     */
    public void onStartup(Session systemSession) {
        try {
            this.systemSession = systemSession;

            // add short id observer
            systemSession.getWorkspace().getObservationManager().addEventListener(
                    new AddedNodeObserver(Constants.TYPE_VERSIONABLE),
                    Event.NODE_ADDED, "/",
                    true,
                    null,
                    null,
                    true);

            systemSession.getWorkspace().getObservationManager().addEventListener(
                    new AddedNodeObserver(Constants.TYPE_VERSIONABLE),
                    Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED, "/",
                    true,
                    null,
                    new String[] {Constants.TYPE_VERSIONABLE},
                    true);
        } catch (RepositoryException ex) {
            Logger.getLogger(CheckinStartupExtension.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Stores a timestamp and a userid to the node, that has been modified. If this is a creation node, this will be done only during node creation,
     * else on every modification, depending on the registered event type.
     */
    class AddedNodeObserver implements EventListener {

        String nodeType = null;

        /**
         * Add the new node to the version repository
         * 
         * @param nodeType If not null, check for this type
         */
        public AddedNodeObserver(String nodeType) {
            this.nodeType = nodeType;
        }

        /**
         * On an NODE_ADDED event, check, if this was the nodeType in question. If true, create a short id and assign it to the new
         * node.
         * 
         * @param events
         */
        public void onEvent(EventIterator events) {
            HashSet<String> processedPathes = new HashSet<String>();
            try {
                Node root = systemSession.getRootNode();

                while (events.hasNext()) {
                    Event event = events.nextEvent();

                    if (processedPathes.contains(event.getPath()))
                        continue;

                    String path = event.getPath().substring(1);

                    if (!root.hasNode(path))
                        continue;
                    
                    Node node = root.getNode(path);
                    if (nodeType != null && !node.isNodeType(nodeType))
                        continue;

                    if (!node.isCheckedOut())
                        continue;

                    node.checkin();
                    node.checkout();
                    
                    processedPathes.add(event.getPath());

                }

                systemSession.save();
            } catch (Throwable ex) {
                Logger.getLogger(CheckinStartupExtension.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        /**
         * Create a short id and return it.
         *
         * The short id will be created from up to three sources:
         *
         * 1. The base id will be determined from the value restriction of the property baseId. This property must be added to any node definition inheriting from nodes:shortId. This is because
         * it is not possible to add a value constraint to the nodes:shortId/nodes:id property directly in the derived node, because further restricting the value of a property in a derived node is not supported.
         * 2. If there are nodes of type nodes:shortIdDemarcator in the path leading to the shortId node, the value of the property nodes:demarcatorId will be added to the shortId. This allows e.g. to
         * distinguish the requirements of different projects.
         * 3. If no qualifier is present in the path, but a property of the shortId node references a node of type nodes:shortIdDemarcator,
         * the value of the property nodes:demarcatorId of the referenced node will be added.
         *
         * The baseId plus optional qualifier id will be made unique using a sequence node.
         *
         * @param node
         * @return
         * @throws RepositoryException
         * @throws InterruptedException
         */
        protected String createShortId(Node node) throws RepositoryException, InterruptedException {
            PropertyDefinition[] propertyDefinitions = node.getPrimaryNodeType().getPropertyDefinitions();
            PropertyDefinition idPropertyDefinition = null;
            for (PropertyDefinition definition : propertyDefinitions) {
                if (definition.getName().equals(Constants.PROP_BASE_ID)) {
                    idPropertyDefinition = definition;
                    break;
                }
            }

            if (idPropertyDefinition == null)
                return null;

            String[] constraints = idPropertyDefinition.getValueConstraints();
            if (constraints.length != 1)
                return null;

            String baseId = constraints[0];
            if (baseId.length() == 0)
                return null;

             String shortId = baseId;
            String qualifier = getPathQualifier(node);
            if (qualifier == null)
                qualifier = getReferenceQualifier(node);
            if (qualifier != null)
                shortId = qualifier + Constants.SHORT_ID_SEPARATOR + baseId;

            /* node may already have a short id. These may be either a correct short id (e.g. after a move) or a short id violating the constraint entered by the user.
             * In the former case, do not change the id. In the latter case, overwrite it using a correct id.
             */
            if (node.hasProperty(Constants.PROP_SHORT_ID) && node.getProperty(Constants.PROP_SHORT_ID).getString().startsWith(shortId))
                return null;

           long sequenceNumber = getSequenceNumber(shortId);
            if (sequenceNumber < 0)
                return null;

            shortId = shortId + Constants.SHORT_ID_SEPARATOR + sequenceNumber;

            return shortId;
        }

        /**
         * Search the path upwards from node for ancestor nodes of type nodes:shortIdDemarcator. The demarcatorId of each such
         * node will prepended to the short id, separated from each other by the separator.
         *
         * @param node
         * @return
         */
        protected String getPathQualifier(Node node) throws RepositoryException {
            Node root = systemSession.getRootNode();

            Stack<String> qualifiers = new Stack<String>();
            node = node.getParent();
            while (!node.getPath().equals(root.getPath())) {
                if (node.isNodeType(Constants.TYPENAME_SHORT_ID_DEMARCATOR) && node.hasProperty(Constants.PROP_DEMARCATOR_ID))
                    qualifiers.push(node.getProperty(Constants.PROP_DEMARCATOR_ID).getString());
                node = node.getParent();
            }

            if (qualifiers.size() == 0)
                return null;

            StringBuffer qualifier = new StringBuffer();
            while (qualifiers.size() > 0) {
                String qualifierComponent = qualifiers.pop();
                if (qualifiers.size() > 0)
                    qualifier.append(qualifierComponent + Constants.SHORT_ID_SEPARATOR);
                else
                    qualifier.append(qualifierComponent);
            }

            return qualifier.toString();
        }

        /**
         * Search for a supertype of the current node, that has a single reference property with a value constraint of
         * reference to nodes:shortIdDemarcator.
         *
         * This can be used, if the hierarchy of nodes is not expressed in the path, but using reference. E.g. imagine a
         * requirement referring to the associated project.
         *
         * @param node
         * @return
         */
        protected String getReferenceQualifier(Node node) throws RepositoryException {
            NodeType[] supertypes = node.getPrimaryNodeType().getSupertypes();

            for (NodeType supertype : supertypes) {
                if (supertype.getDeclaredPropertyDefinitions().length != 1)
                    continue;

                PropertyDefinition propertyDefinition = supertype.getDeclaredPropertyDefinitions()[0];
                if (propertyDefinition.getRequiredType() != PropertyType.REFERENCE)
                    continue;

                String[] valueConstraints = propertyDefinition.getValueConstraints();
                if (valueConstraints.length != 1 && !valueConstraints[0].equals(Constants.TYPENAME_SHORT_ID_DEMARCATOR))
                    continue;

                if (!node.hasProperty(propertyDefinition.getName()))
                    return null;

                Node shortIdDemarcator = systemSession.getNodeByUUID(node.getProperty(propertyDefinition.getName()).getString());
                return shortIdDemarcator.getProperty(Constants.PROP_DEMARCATOR_ID).getString();
            }

            return null;
        }

        protected long getSequenceNumber(String sequenceName) throws RepositoryException, InterruptedException {
            Node root = systemSession.getRootNode();
            String baseIdPath = Constants.DIR_NODES + Constants.PATH_SEPARATOR + Constants.CHILD_SHORT_IDS + Constants.PATH_SEPARATOR;
            RepositoryModel repositoryModel = new RepositoryModel(systemSession);
            Node sequenceParent = repositoryModel.getNode(root, baseIdPath);
            Node sequenceNode;
            if (!sequenceParent.hasNode(sequenceName)) {
                sequenceNode = sequenceParent.addNode(sequenceName, Constants.TYPENAME_SEQUENCE);
                systemSession.save();
            } else
                sequenceNode = sequenceParent.getNode(sequenceName);

            long sequenceNumber = 1L;
            boolean locked = false;
            try {
                while (!locked) {
                    try {
                        sequenceNode.lock(false, true);
                        locked = true;
                    } catch (UnsupportedRepositoryOperationException ex) {
                        return -1L;
                    } catch (LockException ex) {
                        sleepRandom();
                        continue;
                    }
                }

                if (sequenceNode.hasProperty(Constants.PROP_SEQUENCE_NUMBER))
                    sequenceNumber = sequenceNode.getProperty(Constants.PROP_SEQUENCE_NUMBER).getLong();
                sequenceNode.setProperty(Constants.PROP_SEQUENCE_NUMBER, sequenceNumber + 1);
                systemSession.save();
            } finally {
                if (locked)
                    sequenceNode.unlock();
            }

            return sequenceNumber;
        }

        /**
         * Let the current thread sleep an arbitrary amount of time,
         * up to a second.
         * 
         * @throws InterruptedException
         */
        protected void sleepRandom() throws InterruptedException {
            Random generator = new Random(Thread.currentThread().getId());
            int millis = generator.nextInt() % 1000;

            Thread.sleep(millis);
        }
    }
}
